﻿//-----------------------------------------------------------------------
// <copyright file="PluginManager.cs" author="Artak Mkrtchyan">
//     Creative Commons - GNU General Public License (http://creativecommons.org/licenses/GPL/2.0/
// </copyright>
// <author>Artak Mkrtchyan</author>
// <email>mkArtak@gmail.com</email>
// <date>15/07/2012</date>
//-----------------------------------------------------------------------

using ConsoleHoster.Common.Plugins;
using ConsoleHoster.Common.Utilities;
using ConsoleHoster.Model.Entities;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

namespace ConsoleHoster.Model.Plugins
{
	internal sealed class PluginManager : IPluginManager
	{
		private readonly IList<Tuple<AppDomain, IPlugin>> loadedPlugins = new List<Tuple<AppDomain, IPlugin>>();
		private readonly ILogger logger;
		private readonly IStorageManager storageManager;

		private ReadOnlyCollection<PluginDetails> availablePlugins;

		public PluginManager(ILogger argLogger, IStorageManager argStorageManager)
		{
			ValidationManager.RequireArgumentNotNull(argStorageManager, "argStorageManager");

			this.logger = new ComponentLogger("PluginManager", argLogger);
			this.storageManager = argStorageManager;
		}

		public void Initalize()
		{
			IEnumerable<PluginDetails> tmpLoadedPlugins = this.storageManager.LoadPlugins();
			if (tmpLoadedPlugins != null)
			{
				this.availablePlugins = tmpLoadedPlugins.ToList().AsReadOnly();
			}
		}

		public IPlugin LoadPlugin(string argPluginName)
		{
			this.logger.LogMessage("Preparing to load plugin: {0}", argPluginName);

			IPlugin tmpResult = this.loadedPlugins.Where(item => item.Item2.Name.Equals(argPluginName, StringComparison.InvariantCultureIgnoreCase)).Select(item => item.Item2).SingleOrDefault();
			if (tmpResult != null)
			{
				this.logger.LogMessage("Found plugin {0}. Preparing to load", argPluginName);
				PluginDetails tmpDetails = this.GetPluginDetails(argPluginName);

				if (tmpDetails == null)
				{
					this.logger.LogError("Unable to load plugin '{0}' details", argPluginName);
				}
				else
				{
					this.logger.LogMessage("Plugin '{0}' details were foud. Trying to load plugin...");

					try
					{
						tmpResult = this.CreatePluginInAppDomain(tmpDetails);
					}
					catch (Exception ex)
					{
						this.logger.LogError("Error while trying to create plugin in a separate AppDomain. Plugin name: {0}, Error Details: {1}", argPluginName, ex.ToString());
					}
				}
			}
			else
			{
				this.logger.LogWarning("Plugin {0} was not found for loading", argPluginName);
			}

			return tmpResult;
		}

		public void UnloadPlugin(string argPluginName)
		{
			if (!this.IsPluginLoaded(argPluginName))
			{
				throw new ApplicationException(String.Format("Plugin {0} is not loaded", argPluginName));
			}
			Tuple<AppDomain, IPlugin> tmpInfo = this.GetLoadedPluginInfo(argPluginName);

			this.UnloadPlugin(tmpInfo.Item2, tmpInfo.Item1);
		}

		private bool IsPluginLoaded(string argPluginName)
		{
			return this.GetLoadedPluginInfo(argPluginName) != null;
		}

		private Tuple<AppDomain, IPlugin> GetLoadedPluginInfo(string argPluginName)
		{
			return this.loadedPlugins.Where(item => item.Item2.Name.Equals(argPluginName, StringComparison.InvariantCultureIgnoreCase)).SingleOrDefault();
		}

		public void UnloadPlugins()
		{
			while (this.loadedPlugins.Any())
			{
				var tmpPluginToRemove = this.loadedPlugins.FirstOrDefault();
				if (tmpPluginToRemove != null)
				{
					this.loadedPlugins.Remove(tmpPluginToRemove);
					this.UnloadPlugin(tmpPluginToRemove.Item2, tmpPluginToRemove.Item1);
				}
			}
		}

		private void UnloadPlugin(IPlugin argPlugin, AppDomain argPluginDomain)
		{
			if (argPlugin != null)
			{
				argPlugin.Unload();
			}

			if (argPluginDomain != null)
			{
				AppDomain.Unload(argPluginDomain);
			}
		}

		private PluginDetails GetPluginDetails(string argPluginName)
		{
			return availablePlugins.FirstOrDefault(item => item.Name == argPluginName);
		}

		private IPlugin CreatePluginInAppDomain(PluginDetails argDetails)
		{
			IPlugin tmpResult = null;
			AppDomain tmpPluginDomain = null;
			try
			{
				tmpPluginDomain = AppDomain.CreateDomain(String.Format("Plugin{0}", argDetails.Name));
				tmpResult = tmpPluginDomain.CreateInstanceAndUnwrap(argDetails.AssemblyFullName, argDetails.PluginTypeName) as IPlugin;
				this.loadedPlugins.Add(Tuple.Create(tmpPluginDomain, tmpResult));
				tmpResult.Load();
			}
			catch (Exception ex)
			{
				this.logger.LogError("Error while creating plugin {0} in its own AppDomain. Details: {1}", argDetails.Name, ex.ToString());
				this.UnloadPlugin(tmpResult, tmpPluginDomain);
				throw;
			}

			return tmpResult;
		}

		public IEnumerable<PluginDetails> AvailablePlugins
		{
			get
			{
				return this.availablePlugins;
			}
		}

		public IEnumerable<PluginDetails> AutostartPluginNames
		{
			get
			{
				IEnumerable<PluginDetails> tmpResult = null;
				if (this.AvailablePlugins != null)
				{
					tmpResult = this.AvailablePlugins.Where(item => item.AutoLoad).ToList();
				}

				return tmpResult;
			}
		}
	}
}
